package kernel.loaders.ascii;

import java.io.IOException;
import kernel.stl.Facet;
import kernel.stl.STL;
import kernel.stl.Solid;
import kernel.stl.PointXYZ;
import kernel.stl.Vector;

/**Faz a leitura de um arquivo STL no formato ASCII.
 * Como resultado da leitura, esta classe cria um objeto do tipo STL que
 * representa o sólido obtido.
 * @author George
 */
public class ASCIISTLParser {

    private ASCIIScanner scanner;
    private Token currentToken;

    /**Construtor da classe. 
     * @param scanner scanner que será utilizado para ler caracteres a partir
     * do arquivo fonte.
     * @throws java.io.IOException
     */
    public ASCIISTLParser(ASCIIScanner scanner) throws IOException {
        this.scanner = scanner;
        acceptIt();
    }
   
    /**Verifica se o caracter atual é do tipo esperado. 
     * @param kind tipo do esperado.
     * @throws java.io.IOException caso ocorra algum erro na leitura do arquivo
     * fonte.
     * @throws kernel.loaders.ascii.UnexpectedTokenException caso um token
     * inesperado seja encontrado.
     * @see kernel.loaders.ascii.token
     */
    private void accept(int kind) throws IOException, UnexpectedTokenException {

        if (currentToken.kind == (kind)) {
            currentToken = scanner.scan();
        } else {
            UnexpectedTokenException unexpectedTokenException = new UnexpectedTokenException();
            throw unexpectedTokenException;
        }
    }

    /**Avalia se o token atual é do tipo Double e caso seja, é retornado o valor
     * double correspondente. 
     * @return valor do tipo double lido do token atual.
     * @throws java.io.IOException caso ocorra algum erro na leitura do arquivo
     * fonte.
     * @throws kernel.loaders.ascii.UnexpectedTokenException caso um token
     * com tipo diferente de double seja o atual.
     */
    private double acceptDouble() throws IOException, UnexpectedTokenException {
        double r = 0;
        if (currentToken.kind == Token.FLOAT) {
            r = Double.parseDouble(currentToken.spelling);
            currentToken = scanner.scan();
        } else {
            UnexpectedTokenException unexpectedTokenException = new UnexpectedTokenException();
            throw unexpectedTokenException;
        }

        return r;
    }

    /**Avalia se o token atual possui uma determinada sequência de caracteres.
     * Caso a sequência seja a mesma, então o token atual é aceito. 
     * @param spelling sequência de caracteres a ser avaliada.
     * @param caseSensitive indica se a comparação é case sensitive.
     * @throws java.io.IOException Caso ocorra algum erro na leitura do arquivo
     * fonte.
     * @throws kernel.loaders.ascii.UnexpectedTokenException Caso o token atual
     * não tenha a mesma sequência de caracteres especificada no parâmetro spelling.
     */
    private void accept(String spelling, boolean caseSensitive) throws IOException, UnexpectedTokenException {
        if (caseSensitive && currentToken.spelling.equals(spelling)) {
            currentToken = scanner.scan();
        } else if (!caseSensitive && currentToken.spelling.toLowerCase().equals(spelling.toLowerCase())) {
            currentToken = scanner.scan();
        } else {
            UnexpectedTokenException unexpectedTokenException = new UnexpectedTokenException();
            throw unexpectedTokenException;
        }
    }

    /**Aceita o caracter atual, independente do seu tipo ou sequência de caracteres.
     * @throws java.io.IOException Caso ocorra algum erro na leitura do arquivo
     * fonte.
     */
    private void acceptIt() throws IOException {
        currentToken = scanner.scan();
    }

    /**Dispara o processo de leitura e após a sua conclusão retorna um objeto STL. 
     * @return Arquivo STL lido.
     * @throws java.io.IOException Caso ocorra algum erro de leitura.
     */
    public STL parseSTL() throws IOException {
        try {
            accept("solid", false);

            String solidName = "";
            Solid solid;
            Facet facet;

            //name é opcional
            if (!currentToken.spelling.toLowerCase().equals("facet")) {
                solidName = currentToken.spelling;
                acceptIt();
            }

            solid = new Solid(solidName);

            while (currentToken.spelling.toLowerCase().equals("facet")) {
                facet = parseFacet();
                solid.addFacet(facet);
            }

            accept("endsolid", false);

            //A especificação do formato STL não deixa claro se o parâmetro name 
            //do final do arquivo deve ser igual ao parâmetro name do início. Para 
            //obrigar que eles sejam iguais, basta descomentar a chamada accept 
            //e comentar a chamada AcceptIt deve ser comentada.
            
            //accept(solidName, false);
            acceptIt();

            return new STL(solid);
        } catch (UnexpectedTokenException unexpectedTokenException) {
            IOException ioException = new IOException();
            throw ioException;
        }
    }

    /**Lê os próximos tokens do arquivo até que uma nova faceta seja
     * completamente criada.
     * @return faceta lida.
     * @throws java.io.IOException Caso ocorra algum erro la leitura do arquivo
     * fonte.
     * @throws kernel.loaders.ascii.UnexpectedTokenException Caso um token 
     * inesperado seja encontrado ao longo do arquivo.
     */
    private Facet parseFacet() throws IOException, UnexpectedTokenException {
        /* facet normal n1 n2 n3
         *   outer loop
         *     vertex v11 v12 v13
         *     vertex v21 v22 v23
         *     vertex v31 v32 v33
         *   endloop
         * endfacet
         */
        accept("facet", false);

        Vector normal = parseNormal();

        accept("outer", false);
        accept("loop", false);

        PointXYZ vertex1 = parseVertex();
        PointXYZ vertex2 = parseVertex();
        PointXYZ vertex3 = parseVertex();

        accept("endloop", false);
        accept("endfacet", false);

        return new Facet(vertex1, vertex2, vertex3, normal);
    }

    /**Método chamado quando o parâmetro name deve ser lido.
     * Para este parâmetro, o token atual deve ser do tipo identificador.
     * @return nome lido.
     * @throws java.io.IOException Caso ocorra um erro na leitura do arquivo 
     * fonte.
     */
    private String parseName() throws IOException {
        String name = "";
        if (this.currentToken.kind == Token.IDENTIFIER) {
            name = currentToken.spelling;
            acceptIt();
        } else {
            //lançar exceção
        }
        return name;
    }

    /**Lê os próximos tokens do arquivo fonte até que um novo vértice seja lido.
     * @return Vértice lido.
     * @throws java.io.IOException Caso ocorra algum erro na leitura do arquivo
     * fonte.
     * @throws kernel.loaders.ascii.UnexpectedTokenException Caso um token 
     * inesperado seja encontrado.
     */
    private PointXYZ parseVertex() throws IOException, UnexpectedTokenException {
        accept("vertex", false);

        double x = acceptDouble();
        double y = acceptDouble();
        double z = acceptDouble();

        return new PointXYZ(x, y, z);
    }
    
    /**Lê os próximos tokens do arquivo fonte até que um novo vetor seja lido.
     * @return Vetor lido.
     * @throws java.io.IOException Caso ocorra algum erro na leitura do arquivo.
     * @throws kernel.loaders.ascii.UnexpectedTokenException Caso um token
     * inesperado seja encontrado.
     */
    private Vector parseNormal() throws IOException, UnexpectedTokenException {
        accept("normal", false);

        double x = acceptDouble();
        double y = acceptDouble();
        double z = acceptDouble();

        return new Vector(x, y, z);
    }
}
